/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.hamacekj.geometr;

import cz.hamacekj.geometr.plugin.ConstructionObject;
import cz.hamacekj.geometr.plugin.ICircleObject;
import cz.hamacekj.geometr.plugin.ILineObject;
import cz.hamacekj.geometr.plugin.IPoint;
import cz.hamacekj.geometr.plugin.IPointObject;
import cz.hamacekj.geometr.plugin.IRayObject;
import cz.hamacekj.geometr.plugin.ISegmentObject;
import cz.hamacekj.geometr.plugin.PrimitiveObject;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.PrintJob;
import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D;
import java.awt.print.PageFormat;
import java.awt.print.PrinterJob;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.Collection;
import javax.swing.ListSelectionModel;
import javax.swing.Painter;

/**
 * Třída, která se stará o vykreslování kroků do nákresu.
 */
public class ConstructionPainter implements Painter<Component>{
    private StepsModel stepsModel1;
    private ListSelectionModel stepsSelection;
    private double lengthRatio;
    private IPoint relativeStart;
    private volatile boolean paint = true;
    private Color labelColor = Color.BLUE;
    private ToolBox toolbox = null;
    private boolean refreshLabelPlacement = true;
    private boolean printableAreaVisible = false;
    private PageFormat pageFormat;
    
    private Collection<LabelPainter> labelPlaces;

    public ConstructionPainter(StepsModel stepsModel1, ListSelectionModel stepsSelection, double lengthRatio, IPoint startPoint) {
        this.stepsModel1 = stepsModel1;
        this.lengthRatio = lengthRatio;
        this.relativeStart = startPoint;
        this.stepsSelection = stepsSelection;
        PrinterJob p = PrinterJob.getPrinterJob();
        pageFormat = p.defaultPage();
    }

    /**
     * Nastaví jestli bude komponenta vykreslovat.
     * @param paint true, pokud má vykreslovat.
     */
    public void setPaint(boolean paint){
        this.paint = paint;
    }

    /**
     * Vrátí hodnotu, jestli tato komponenta právě vykresluje.
     * @return true pokud vykresluje.
     */
    public boolean getPaint(){
        return this.paint;
    }
    
    /**
     * Nastaví hodnotu, jestli je zrovna vidět náhled tisknuté oblasti.
     * @param visible true, pokud je vidět náhled.
     */
    public void setPrintableAreaVisible(boolean visible){
        this.printableAreaVisible = visible;
    }
    
    /**
     * Vrátí hodnotu, jetstli je zrovna vidět náhled tisknuté oblasti.
     * @return true, pokud je vidět náhled.
     */
    public boolean getPrintableAreaVisible(){
        return this.printableAreaVisible;
    }
    
    /**
     * Vrátí formát stránky, pomocí kterého se zobrazuje případný náhled tisknuté oblasti.
     * @return formát stránky.
     */
    public PageFormat getPageFormat(){
        return pageFormat;
    }
    
    /**
     * Nastaví formát stránky, pomocí kterého se zobrazuje případný náhled tisknuté oblasti.
     * @param f formát stránky.
     */
    public void setPageFormat(PageFormat f){
        this.pageFormat = f;
    }
    
    private void paintPrintableArea(Graphics2D g){
        Paint backup = g.getPaint();
        g.setPaint(Color.DARK_GRAY);
        int width = (int)pageFormat.getImageableWidth();
        int height = (int)pageFormat.getImageableHeight();
        g.drawRect(0, 0, width, height);
        g.setPaint(backup);
    }

    /**
     * Kreslí pomocí g na object kroky stepsModelu až do toho, který je vybrán.
     * Kreslí jen ty, které nemají nastavenou viditelnost na hidden. Zatím nedělá rozdíly zvýraznění.
     * @param g Kreslící nástroj
     * @param object konfigurační objekt. Implementací je ignorovaný.
     * @param width šířka nákresu
     * @param height výška nákresu
     */
    @Override
    public void paint(Graphics2D g, Component object, int width, int height) {
        if(!this.paint){
            return;
        }
        if(this.printableAreaVisible){
            this.paintPrintableArea(g);
        }
        g.setPaint(Color.BLACK);
        if(this.refreshLabelPlacement){
            this.doRefreshLabelPlacement(g, width, height);
            this.refreshLabelPlacement = false;
        }
        int i = 0;
        int maxSelected = stepsSelection.getMaxSelectionIndex();
        for(Step s: stepsModel1){
            if(s.getVisibility() == Step.HIDDEN){
                continue;
            }
            for(ConstructionObject o: s.getConstructionObjects()){
                for(PrimitiveObject p: o.getPrimitiveObjects()){
                    if(p instanceof IPointObject){
                        java.awt.Point point = translatePoint(((IPointObject) p).getPoint());
                        g.drawLine(point.x-5, point.y-5, point.x+5, point.y+5);
                        g.drawLine(point.x-5, point.y+5, point.x+5, point.y-5);
                    }
                    if(p instanceof ISegmentObject){
                        ISegmentObject segment = (ISegmentObject) p;
                        java.awt.Point start = translatePoint(segment.getA());
                        java.awt.Point end = translatePoint(segment.getB());
                        g.drawLine(start.x, start.y, end.x, end.y);
                    }
                    if(p instanceof ICircleObject){
                        ICircleObject circ = (ICircleObject) p;
                        java.awt.Point center = translatePoint(circ.getCenter());
                        int radius = (int) (circ.getRadius() * lengthRatio);
                        g.drawArc(center.x - radius, center.y - radius, 2*radius, 2*radius, 0, 360);
                    }
                    if(p instanceof IRayObject){
                        IRayObject ray = (IRayObject) p;
                        java.awt.Point start = translatePoint(ray.getA());
                        java.awt.Point end = translatePoint(ray.getB());
                        java.awt.Point realEnd = getOuterPoint(start, end, width, height);
                        g.drawLine(start.x, start.y, realEnd.x, realEnd.y);
                    }
                    if(p instanceof ILineObject){
                        ILineObject line = (ILineObject) p;
                        java.awt.Point start = translatePoint(line.getA());
                        java.awt.Point end = translatePoint(line.getB());
                        java.awt.Point realStart = getOuterPoint(start, end, width, height);
                        java.awt.Point realEnd = getOuterPoint(end, start, width, height);
                        g.drawLine(realStart.x, realStart.y, realEnd.x, realEnd.y);
                    }
                }
            }
            if(maxSelected == i++){
                break;
            }
        }
        g.setPaint(this.labelColor);
        if(this.labelPlaces != null){
            for (LabelPainter p : this.labelPlaces){
                p.paintLabel(g);
            }
        }
    }
    
    /**
     * Nastaví příznak, že při příštím repaintu se budou přepočítávat pozice popisků.
     */
    public void refreshLabelPlacement(){
        this.refreshLabelPlacement = true;
    }
    
    /**
     * Provede přepočítání pozic popisků.
     * @param g grafický kontext k výpočtu metrik textu.
     * @param width šířka zobrazení.
     * @param height výška zobrazení.
     */
    private void doRefreshLabelPlacement(Graphics2D g, int width, int height){
        if(this.labelPlaces == null){
            this.labelPlaces = new ArrayList<>(this.stepsModel1.getSize() * 2);
        }else{
            this.labelPlaces.clear();
        }
        ToolBox toolbox = this.getToolbox();
        for(int i = 0; i < stepsModel1.getSize(); i++){
            for(ConstructionObject o : stepsModel1.getElementAt(i).getConstructionObjects()){
                String label = o.getLabel();
                boolean wantLabel = o.isLabelShown() && label != null && !label.isEmpty();
                for(PrimitiveObject obj : o.getPrimitiveObjects()){
                    if(wantLabel && obj instanceof IPointObject){
                        IPoint p = ((IPointObject)obj).getPoint();
                        java.awt.Point place = this.getLabelPlaceNear(translatePoint(p), label, g, width, height);
                        labelPlaces.add(new LabelPainter(place, label));
                        wantLabel = false;
                        break;
                    }
                    if(wantLabel && obj instanceof ISegmentObject){
                        ISegmentObject segment = (ISegmentObject)obj;
                        IPoint a = segment.getA();
                        IPoint b = segment.getB();
                        IPoint center = toolbox.createPoint(
                                (a.getX() + b.getX()) / 2, (a.getY() + b.getY()) / 2);
                        java.awt.Point place = this.getLabelPlaceNear(translatePoint(center), label, g, width, height);
                        labelPlaces.add(new LabelPainter(place, label));
                        wantLabel = false;
                        break;
                    }
                    if(wantLabel && obj instanceof ICircleObject){
                        ICircleObject circle = (ICircleObject)obj;
                        java.awt.Point center = translatePoint(circle.getCenter());
                        java.awt.Point place = this.getLabelPlaceNear(center, label, g, translateLength(circle.getRadius()), width, height);
                        labelPlaces.add(new LabelPainter(place, label));
                        wantLabel = false;
                        break;
                    }
                    if(wantLabel && obj instanceof IRayObject){
                        IRayObject ray = (IRayObject)obj;
                        IPoint a = ray.getA();
                        IPoint b = ray.getB();
                        java.awt.Point inner = getInnerPoint(translatePoint(a), translatePoint(b), width, height);
                        java.awt.Point place = this.getLabelPlaceNear(inner, label, g, width, height);
                        labelPlaces.add(new LabelPainter(place, label));
                        wantLabel = false;
                        break;
                    }
                    if(wantLabel && obj instanceof ILineObject){
                        ILineObject line = (ILineObject)obj;
                        IPoint a = line.getA();
                        IPoint b = line.getB();
                        java.awt.Point inner = getInnerPoint(translatePoint(a), translatePoint(b), width, height);
                        java.awt.Point place = this.getLabelPlaceNear(inner, label, g, width, height);
                        labelPlaces.add(new LabelPainter(place, label));
                        wantLabel = false;
                        break;
                    }
                }
            }
            if(stepsSelection.getMaxSelectionIndex() == i){
                break;
            }
        }
    }

    /**
     * Nastaví barvu, kterou se budou kreslit popisky.
     * @param labelColor
     */
    public void setLabelColor(Color labelColor){
        this.labelColor = labelColor;
    }
    
    /**
     * Vrátí barvu, kterým se kreslí popisky.
     * Vrátí null, pokud ještě žádný font není nastaven (kreslí se výchozím).
     * @return Barva, kterou se kreslí popisky.
     */
    public Color getLabelColor(){
        return this.labelColor;
    }
    
    /**
     * Nastaví "přiblížení" nákresu. Jinak řečeno nastaví poměr pixel/cm na obrazovce.
     * Za cm používám v celém programu jako virtuální jednotku, ve které jsou všechny vzdálenosti nákresu.
     * @param d Poměr pixel/cm na obrazovce.
     */
    public void setLengthRatio(double d){
        this.lengthRatio = d;
        this.refreshLabelPlacement();
    }

    /**
     * Vrátí "přiblížení nákresu".
     * @see #setLengthRatio(double) 
     * @return přiblížení nákresu
     */
    public double getLengthRatio(){
        return this.lengthRatio;
    }

    /**
     * Nastaví levý horní roh v cm.
     * @param p Pozice levého horního rohu v cm.
     */
    public void setRelativeStart(IPoint p){
        this.relativeStart = p;
        this.refreshLabelPlacement();
    }

    /**
     * Vrátí levý horní roh v cm.
     * @return Pozice levého horního rohu v cm.
     */
    public IPoint getRelativeStart(){
        return this.relativeStart;
    }

    /**
     * Nastaví seznam kroků, které se budou vykreslovat.
     * @param stepsModel Seznam kroků. 
     */
    public void setStepsModel(StepsModel stepsModel){
        this.stepsModel1 = stepsModel;
        this.refreshLabelPlacement();
        // WARNING: měl bych zajistit, že bude repaint.
    }
    
    /**
     * Najde místo poblíž p k umístění popisku
     * @param p místo poblíž kterého hledat
     * @param label popisek, který umístit
     * @param g kreslící nástroj (potřeba k měření)
     * @param width šířka kreslícího pole
     * @param height výška kreslícího pole
     * @return místo popisku
     */
    private java.awt.Point getLabelPlaceNear(java.awt.Point p, String label, Graphics2D g
            , int width, int height){
        return this.getLabelPlaceNear(p, label, g, 0, width, height);
    }
    
    /**
     * Najde místo vzdálené alespoň distance od místa p.
     * @param p místo poblíž kterého hledat
     * @param label popisek, který umístit
     * @param g kreslící nástroj
     * @param distance vzdálenost popisku od místa
     * @param width šířka kreslícího pole
     * @param height výška kreslícího pole
     * @return místo popisku
     */
    private java.awt.Point getLabelPlaceNear(java.awt.Point p, String label, Graphics2D g,
            int distance, int width, int height){
        int distance0 = 5 + distance;
        int distance1 = 3+ (int)Math.sqrt(distance*distance / (double)2);
        FontMetrics metr = g.getFontMetrics(g.getFont());
        
        Rectangle2D rect = metr.getStringBounds(label, g);
        Dimension d = new Dimension((int)rect.getWidth(), (int)rect.getHeight());
        java.awt.Point places[] = {
            new java.awt.Point(p.x + distance1, p.y + distance1 + d.height),
            new java.awt.Point(p.x + distance0, p.y + d.height / 2),
            new java.awt.Point(p.x + distance1, p.y - distance1),
            new java.awt.Point(p.x - d.width / 2, p.y - distance0),
            new java.awt.Point(p.x - distance1 - d.width, p.y - distance1),
            new java.awt.Point(p.x - distance0 - d.width, p.y - d.height / 2),
            new java.awt.Point(p.x - distance1 - d.width, p.y + distance1 + d.height),
            new java.awt.Point(p.x - d.width / 2, p.y + distance0 + d.height) };
        for(java.awt.Point place : places){
            if(isPlaceToPaint(place, d, width, height)){
                return place;
            }
        }
        return places[0];
    }
    
    
    /**
     * Vrátí boolean jestli na tomto místě je místo velikosti dimension.
     * @param p Pozice levého horního rohu místa na které chci kreslit.
     * @param d Velikost místa na kterou chci kreslit.
     * @param width šířka kreslícího pole
     * @param height výška kreslícího pole
     * @return true pokud na toto místo lze kreslit bez překreslení ničeho jiného.
     */
    private boolean isPlaceToPaint(java.awt.Point p, Dimension d, int width, int height){
        if(p.x + d.width > width || p.x < 0 || p.y + d.height > height || p.y < 0){
            return false;
        }
        ToolBox toolbox = this.getToolbox();
        double diam = Math.sqrt(d.height*d.height + d.width*d.width);
        for(LabelPainter oLabel : this.labelPlaces){
            java.awt.Point oLabelPoint = oLabel.getPoint();
            int distx = oLabelPoint.x - p.x;
            int disty = oLabelPoint.y - p.y;
            double dist = Math.sqrt((double)(distx*distx + disty*disty));
            if(dist < diam){
                return false;
            }
        }
        IPoint topleft = this.reverseTranslatePoint(p);
        IPoint topright = this.reverseTranslatePoint(new java.awt.Point(p.x + d.width, p.y));
        IPoint bottomleft = this.reverseTranslatePoint(new java.awt.Point(p.x, p.y + d.height));
        IPoint bottomright = this.reverseTranslatePoint(new java.awt.Point(p.x + d.width, p.y + d.height));
        ISegmentObject first = toolbox.createSegmentObject(topleft, bottomright);
        ISegmentObject second = toolbox.createSegmentObject(topright, bottomleft);
        for(PrimitiveObject prim : stepsModel1.getPrimitiveObjectsView()){
            Collection<? extends PrimitiveObject> arr = toolbox.intersect(first, prim);
            if(arr != null && arr.size() > 0){
                return false;
            }
            Collection<? extends PrimitiveObject> arr1 = toolbox.intersect(second, prim);
            if(arr1 != null && arr1.size() > 0){
                return false;
            }
        }
        return true;
    }
    
    private ToolBox getToolbox(){
        if(this.toolbox == null){
            this.toolbox = new ToolBox();
        }
        return this.toolbox;
    }
    
    /**
     * Přeloží délku v interních jednotkách aplikace na délku v px.
     * @param length délka v cm
     * @return délka v px
     */
    private int translateLength(double length){
        return (int)(length * this.lengthRatio);
    }

    /**
     * Použije relativeStart a lengthRatio k přepočítání souřadnic {@link IPoint IPoint}
     * bodu v cm na souřadnice bodu v px na monitoru.
     * @param p bod v cm (interní jednotka aplikace)
     * @return bod v px na obrazovce.
     */
    private java.awt.Point translatePoint(IPoint p){
        double realx = p.getX() - relativeStart.getX();
        double realy = p.getY() - relativeStart.getY();
        return new java.awt.Point((int) (realx * lengthRatio), (int) (realy * lengthRatio));
    }
    
    /**
     * Použije relativeStart a lengthRatio k přepočítání souřadnic bodu na obrazovce
     * na vnitřní reprezentaci souřadnic {@link IPoint IPoint}.
     * @param p bod v px na obrazovce.
     * @return bod v cm (interní jednotka aplikace)
     */
    private IPoint reverseTranslatePoint(java.awt.Point p){
        double abstractx = (p.x / (double)lengthRatio) + relativeStart.getX();
        double abstracty = (p.y / (double)lengthRatio) + relativeStart.getY();
        return new Point(abstractx, abstracty);
    }

    /**
     * Najde nějaký nepříliš vzdálený bod polopřímky start end. Tento bod je ne moc daleko,
     * ale je vně obdélníku daného pomocí width, height.
     * @param start Startovní bod polopřímky.
     * @param end Druhý bod polopřímky.
     * @param width Šířka obdélníku.
     * @param height Výška obdélníku.
     * @return bod polopřímky mimo obdélník.
     */
    private java.awt.Point getOuterPoint(java.awt.Point start, java.awt.Point end, int width, int height){
        int vectorX = end.x - start.x;
        int vectorY = end.y - start.y;
        double koef;
        // ted vypocitam pozici kam mam kreslit poloprimku abych
        // ji urcite nakreslil alespon na kraj vykreslovaneho prostoru.
        if(Math.abs(vectorX) > Math.abs(vectorY)){
            // dodelat nalezeni bodu
            if(vectorX > 0){
                koef = (width - start.x) / ((double) vectorX);
            }else{
                koef = (0 - start.x) / ((double) vectorX);
            }
        }else{
            if(vectorY > 0){
                koef = (height - start.y) / ((double) vectorY);
            }else{
                koef = (0 - start.y) / ((double) vectorY);
            }
        }
        return new java.awt.Point(start.x + (int) (koef*vectorX), start.y + (int) (koef*vectorY));
    }
    
    private java.awt.Point getInnerPoint(java.awt.Point start, java.awt.Point end, int width, int height){
        int vectorX = end.x - start.x;
        int vectorY = end.y - start.y;
        int distance = 40;
        
        double koef1 = 0;
        double koef2 = 0;
        if(vectorX > 0){
            koef1 = (width - distance - start.x) / ((double) vectorX);
        }
        if(vectorX < 0){
            koef1 = ( distance - start.x) / ((double)vectorX);
        }
        if(vectorY > 0){
            koef2 = (height - distance - start.y) / ((double)vectorY);
        }
        if(vectorY < 0){
            koef2 = ( distance - start.y) / ((double)vectorY);
        }
        double koef;
        if(koef1 == 0){
            koef = koef2;
        }else{
            if(koef2 == 0){
                koef = koef1;
            }else{
                assert koef1 > 0 && koef2 > 0;
                koef = Math.min(koef1, koef2);
            }
        }
        return new java.awt.Point(start.x + (int)(koef*vectorX),
                start.y + (int)(koef*vectorY));
        
    }
    
    
}